package com.codeiy.domain;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.codeiy.criteria.Criteria;
import com.codeiy.criteria.CriteriaQuery;
import com.codeiy.criteria.Predicate;
import com.codeiy.criteria.Relation;
import com.codeiy.entity.data.DataWarehouse;
import com.codeiy.entity.meta.MetaField;
import com.codeiy.service.DataWarehouseService;
import com.codeiy.service.MetaFieldService;
import com.codeiy.service.MetaObjectService;
import lombok.extern.slf4j.Slf4j;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.modifier.FieldPersistence;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.FieldAccessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import static com.codeiy.constant.CommonConstants.DEFAULT_PACKAGE;

@Slf4j
@Component
public class MetaModel extends JSONObject {
    private ConcurrentHashMap<String, List<MetaField>> fieldListCache = new ConcurrentHashMap<>();
    private ConcurrentHashMap<String, Class<?>> typeContainer = new ConcurrentHashMap<>();
    @Autowired
    private MetaObjectService metaObjectService;
    @Autowired
    private MetaFieldService metaFieldService;
    @Autowired
    private DataWarehouseService dataWarehouseService;

    public synchronized List<MetaField> getFields(String projectCode, String objectCode) {
        if (StrUtil.isBlank(projectCode)) {
            throw new RuntimeException("projectCode不能为空");
        }
        if (StrUtil.isBlank(objectCode)) {
            throw new RuntimeException("objectCode不能为空");
        }
        String key = projectCode + "." + objectCode;
        List<MetaField> fields = fieldListCache.get(key);
        if (fields != null) {
            return fields;
        }
        MetaField fieldQuery = new MetaField();
        fieldQuery.setProjectCode(projectCode);
        fieldQuery.setObjectCode(objectCode);
        fields = metaFieldService.list(new QueryWrapper<>(fieldQuery));
        // 自动添加id属性
        MetaField id = new MetaField();
        id.setDataType("Long");
        id.setFieldCode("id");
        id.setFieldName("主键");
        fields.add(id);
        fieldListCache.put(key, fields);
        return fields;
    }

    public void clearCache() {
        fieldListCache.clear();
    }

    public void clearCache(String projectCode, String objectCode) {
        fieldListCache.remove(projectCode + "." + objectCode);
    }

    public synchronized Class<?> getType(String projectCode, String objectCode) {
        String key = projectCode + "." + objectCode;
        Class<?> type = typeContainer.get(key);
        if (type != null) {
            return type;
        }
        DynamicType.Builder<?> builder = new ByteBuddy().subclass(Serializable.class).name(DEFAULT_PACKAGE + "." + key);
        List<MetaField> fields = getFields(projectCode, objectCode);
        if (CollectionUtil.isEmpty(fields) || fields.get(0).getFieldCode().equals("id")) {
            throw new RuntimeException(key + "未定义属性");
        }
        for (MetaField field : fields) {
            Class<?> classType;
            switch (field.getDataType()) {
                case "Long":
                    classType = Long.class;
                    break;
                case "Integer":
                    classType = Integer.class;
                    break;
                case "Date":
                    classType = Date.class;
                    break;
                case "String":
                    classType = String.class;
                    break;
                case "Object":
                    classType = Object.class;
                    break;
                case "List":
                default:
                    classType = List.class;
                    break;
            }
            String fieldName = field.getFieldCode();
            String getterName = "get" + StrUtil.upperFirst(fieldName);
            String setterName = "set" + StrUtil.upperFirst(fieldName);
            builder = builder.defineField(field.getFieldCode(), classType, Visibility.PRIVATE, FieldPersistence.TRANSIENT)
//                    .annotateField( AnnotationDescription.Builder.ofType( Transient.class ).build() )
                    .defineMethod(getterName, classType, Visibility.PUBLIC)
                    .intercept(FieldAccessor.ofField(fieldName))
                    .defineMethod(setterName, void.class, Visibility.PUBLIC)
                    .withParameters(classType)
                    .intercept(FieldAccessor.ofField(fieldName));
        }
        type = builder.make().load(getClass().getClassLoader()).getLoaded();
        typeContainer.put(key, type);
        return type;
    }

    public void clearType() {
        typeContainer.clear();
    }

    public void clearType(String projectCode, String objectCode) {
        typeContainer.remove(projectCode + "." + objectCode);
    }

    public Object newInstance(String projectCode, String objectCode) {
        try {
            return getType(projectCode, objectCode).newInstance();
        } catch (InstantiationException e) {
            log.error(e.getMessage(), e);
            throw new RuntimeException(e.getMessage(), e);
        } catch (IllegalAccessException e) {
            log.error(e.getMessage(), e);
            throw new RuntimeException(e.getMessage(), e);
        }
    }

    public Object toModel(String projectCode, String objectCode, DataWarehouse data) {
        List<MetaField> fields = getFields(projectCode, objectCode);
        Object model = newInstance(projectCode, objectCode);
        fields.forEach(field -> {
            if (StrUtil.isNotBlank(field.getRelateToMany())) {
                String relateToMany = field.getRelateToMany();
                ToMany toMany = JSONUtil.toBean(relateToMany, ToMany.class);
                String targetProjectCode = toMany.getProjectCode();
                String targetObjectCode = toMany.getObjectCode();
                List<DataWarehouse> relateManyData = getRelateManyData(projectCode, objectCode, data, toMany);
                if (CollectionUtil.isEmpty(relateManyData)) {
                    return;
                }
                List<Object> relateManyModel = toModelList(targetProjectCode, targetObjectCode, relateManyData);
                BeanUtil.setFieldValue(model, field.getFieldCode(), relateManyModel);
            } else if (StrUtil.isNotBlank(field.getRelateToOne())) {
                String relateToOne = field.getRelateToOne();
                ToOne toOne = JSONUtil.toBean(relateToOne, ToOne.class);
                String targetProjectCode = toOne.getProjectCode();
                String targetObjectCode = toOne.getObjectCode();
                DataWarehouse relateData = getRelateOneData(projectCode, objectCode, data, toOne);
                Object relateOneModel = toModel(targetProjectCode, targetObjectCode, relateData);
                BeanUtil.setFieldValue(model, field.getFieldCode(), relateOneModel);
            } else {
                String key = field.getFieldCode();
                Object value = BeanUtil.getFieldValue(data, "fieldValue" + field.getDataFieldPos());
                BeanUtil.setFieldValue(model, key, value);
            }
        });
        BeanUtil.setFieldValue(model, "id", data.getDataId());
        return model;
    }

    private DataWarehouse getRelateOneData(String sourceProjectCode, String sourceObjectCode, DataWarehouse sourceData, ToOne toOne) {
        DataQueryBuilder builder = new DataQueryBuilder();
        builder.sourceData(sourceData).sourceProjectCode(sourceProjectCode).sourceObjectCode(sourceObjectCode).sourceField(toOne.getSourceField())
                .targetProjectCode(toOne.getProjectCode()).targetObjectCode(toOne.getObjectCode()).targetField(toOne.getTargetField());
        return builder.getDataWarehouse();
    }

    private List<DataWarehouse> getRelateManyData(String sourceProjectCode, String sourceObjectCode, DataWarehouse sourceData, ToMany toMany) {
        DataQueryBuilder builder = new DataQueryBuilder();
        builder.sourceData(sourceData).sourceProjectCode(sourceProjectCode).sourceObjectCode(sourceObjectCode).sourceField(toMany.getSourceField());
        builder.targetProjectCode(toMany.getProjectCode());
        if (ToMany.RELATE_TYPE_MIDDLE.equals(toMany.getRelateType())) {
            // 先取中间表数据
            builder.targetObjectCode(toMany.getMiddleObjectCode()).targetField(toMany.getTargetField());
            List<Object> middleDataList = builder.listMiddleFieldData();
            if (CollectionUtil.isEmpty(middleDataList)) {
                return Collections.EMPTY_LIST;
            }
            builder.targetObjectCode(toMany.getObjectCode()).targetField(toMany.getTargetField());
            return builder.listDataWarehouse(middleDataList);
        } else {
            builder.targetObjectCode(toMany.getObjectCode()).targetField(toMany.getTargetField());
            return builder.listDataWarehouse();
        }

    }

    public List<Object> toModelList(String projectCode, String objectCode, List<DataWarehouse> dataList) {
        if (CollectionUtil.isEmpty(dataList)) {
            return Collections.EMPTY_LIST;
        }
        List<Object> objects = new ArrayList<>(dataList.size());
        dataList.forEach(data -> {
            Object object = toModel(projectCode, objectCode, data);
            objects.add(object);
        });
        return objects;
    }

    public DataWarehouse toData(String projectCode, String objectCode, Object bean) {
        DataWarehouse data = new DataWarehouse();
        List<MetaField> fields = getFields(projectCode, objectCode);
        fields.forEach(field -> {
            String key = field.getFieldCode();
            Object value = BeanUtil.getFieldValue(bean, key);
            BeanUtil.setFieldValue(data, "fieldValue" + field.getDataFieldPos(), value);
        });
        return data;
    }

    public QueryWrapper<DataWarehouse> getQueryWrapper(String projectCode, String objectCode, CriteriaQuery criteriaQuery) {
        List<MetaField> fields = getFields(projectCode, objectCode);
        Map<String, MetaField> fieldMap = fields.stream().collect(Collectors.toMap(MetaField::getFieldCode, c -> c));
        criteriaQuery.getCriteriaList().forEach(criteria -> {
            String fieldName = criteria.getField();
            if (fieldName.contains(Criteria.SEPARATOR)) {
                String[] nameArray = fieldName.split(Criteria.SEPARATOR);
                StringBuffer buffer = new StringBuffer();
                for (String name : nameArray) {
                    MetaField metaField = fieldMap.get(name);
                    if (metaField != null) {
                        buffer.append("fieldValue" + metaField.getDataFieldPos() + "_");
                    }
                }
                criteria.setField(buffer.toString());
            } else {
                MetaField metaField = fieldMap.get(fieldName);
                if (metaField != null) {
                    String newFieldName = "fieldValue" + metaField.getDataFieldPos();
                    criteria.setField(newFieldName);
                }
            }
        });
        return criteriaQuery.toQueryWrapper();
    }

    public class DataQueryBuilder {
        private String targetProjectCode;
        private String targetObjectCode;
        private String targetField;
        private List<MetaField> targetFields;
        private Map<String, String> sourceFieldNameMap;

        private DataWarehouse sourceData;
        private String sourceProjectCode;
        private String sourceObjectCode;
        private String sourceField;
        private List<MetaField> sourceFields;
        private Map<String, String> targetFieldNameMap;

        public DataQueryBuilder targetProjectCode(String targetProjectCode) {
            this.targetProjectCode = targetProjectCode;
            return this;
        }

        public DataQueryBuilder targetObjectCode(String targetObjectCode) {
            this.targetObjectCode = targetObjectCode;
            return this;
        }

        public DataQueryBuilder targetField(String targetField) {
            this.targetField = targetField;
            return this;
        }

        public DataQueryBuilder sourceData(DataWarehouse sourceData) {
            this.sourceData = sourceData;
            return this;
        }

        public DataQueryBuilder sourceProjectCode(String sourceProjectCode) {
            this.sourceProjectCode = sourceProjectCode;
            return this;
        }

        public DataQueryBuilder sourceObjectCode(String sourceObjectCode) {
            this.sourceObjectCode = sourceObjectCode;
            return this;
        }

        public DataQueryBuilder sourceField(String sourceField) {
            this.sourceField = sourceField;
            return this;
        }

        private List<MetaField> targetFields() {
            if (targetFields == null) {
                targetFields = getFields(targetProjectCode, targetObjectCode);
            }
            return targetFields;
        }

        private List<MetaField> sourceFields() {
            if (sourceFields == null) {
                sourceFields = getFields(sourceProjectCode, sourceObjectCode);
            }
            return sourceFields;
        }

        private Map<String, String> sourceFieldNameMap() {
            if (sourceFieldNameMap == null) {
                sourceFieldNameMap = sourceFields().stream().collect(Collectors.toMap(MetaField::getFieldCode, c -> "fieldValue" + c.getDataFieldPos()));
            }
            return sourceFieldNameMap;
        }

        private Map<String, String> targetFieldNameMap() {
            if (targetFieldNameMap == null) {
                targetFieldNameMap = targetFields().stream().collect(Collectors.toMap(MetaField::getFieldCode, c -> "fieldValue" + c.getDataFieldPos()));
            }
            return targetFieldNameMap;
        }

        private void check() {
            if (StrUtil.isBlank(targetProjectCode)) {
                throw new RuntimeException("targetProjectCode未设置");
            }
            if (StrUtil.isBlank(targetObjectCode)) {
                throw new RuntimeException("targetObjectCode未设置");
            }
            if (StrUtil.isBlank(targetField)) {
                throw new RuntimeException("targetField未设置");
            }
            if (ObjUtil.isEmpty(sourceData)) {
                throw new RuntimeException("sourceData未设置");
            }
            if (StrUtil.isBlank(sourceProjectCode)) {
                throw new RuntimeException("sourceProjectCode未设置");
            }
            if (StrUtil.isBlank(sourceObjectCode)) {
                throw new RuntimeException("sourceObjectCode未设置");
            }
            if (StrUtil.isBlank(sourceField)) {
                throw new RuntimeException("sourceField未设置");
            }

            String sourceFieldValueField = sourceFieldNameMap().get(sourceField);
            if (StrUtil.isBlank(sourceFieldValueField)) {
                throw new RuntimeException("无效的源字段： " + sourceField);
            }
            Object sourceFieldValue = BeanUtil.getFieldValue(sourceData, sourceFieldValueField);
            if (ObjUtil.isEmpty(sourceFieldValue)) {
                throw new RuntimeException("源字段: " + sourceField + " 值为空");
            }
            String targetFieldValueField = targetFieldNameMap().get(targetField);
            if (StrUtil.isBlank(targetFieldValueField)) {
                throw new RuntimeException("无效的目标字段： " + targetField);
            }
        }

        QueryWrapper<DataWarehouse> dataWarehouseQuery() {
            check();
            String sourceFieldValueField = sourceFieldNameMap().get(sourceField);
            Object sourceFieldValue = BeanUtil.getFieldValue(sourceData, sourceFieldValueField);
            String targetFieldValueField = targetFieldNameMap().get(targetField);
            CriteriaQuery criteriaQuery = new CriteriaQuery();
            List<Criteria> criteriaList = new ArrayList<>();
            criteriaList.add(new Criteria("projectCode", Predicate.EQ, targetProjectCode, Relation.AND));
            criteriaList.add(new Criteria("objectCode", Predicate.EQ, targetObjectCode, Relation.AND));
            criteriaList.add(new Criteria(targetFieldValueField, Predicate.EQ, sourceFieldValue.toString(), Relation.AND));
            criteriaQuery.setCriteriaList(criteriaList);
            return getQueryWrapper(targetProjectCode, targetObjectCode, criteriaQuery);
        }

        DataWarehouse getDataWarehouse() {
            return dataWarehouseService.getOne(dataWarehouseQuery());
        }

        List<DataWarehouse> listDataWarehouse() {
            return dataWarehouseService.list(dataWarehouseQuery());
        }

        List<Object> listMiddleFieldData() {
            String sourceFieldValueField = sourceFieldNameMap().get(sourceField);
            Object sourceFieldValue = BeanUtil.getFieldValue(sourceData, sourceFieldValueField);
            // 从中间表获取数据，使用源字段进行过滤查询
            String middleFieldValueField = targetFieldNameMap().get(sourceField);
            CriteriaQuery criteriaQuery = new CriteriaQuery();
            List<Criteria> criteriaList = new ArrayList<>();
            criteriaList.add(new Criteria("projectCode", Predicate.EQ, targetProjectCode, Relation.AND));
            criteriaList.add(new Criteria("objectCode", Predicate.EQ, targetObjectCode, Relation.AND));
            criteriaList.add(new Criteria(middleFieldValueField, Predicate.EQ, sourceFieldValue.toString(), Relation.AND));
            criteriaQuery.setCriteriaList(criteriaList);
            QueryWrapper<DataWarehouse> queryWrapper = getQueryWrapper(targetProjectCode, targetObjectCode, criteriaQuery);
            List<DataWarehouse> middleDataList = dataWarehouseService.list(queryWrapper);
            String targetFieldValueField = targetFieldNameMap().get(targetField);
            return middleDataList.stream().map(d -> BeanUtil.getFieldValue(d, targetFieldValueField)).collect(Collectors.toList());
        }

        List<DataWarehouse> listDataWarehouse(List<Object> middleFieldData) {
            check();
            CriteriaQuery criteriaQuery = new CriteriaQuery();
            List<Criteria> criteriaList = new ArrayList<>();
            criteriaList.add(new Criteria("projectCode", Predicate.EQ, targetProjectCode, Relation.AND));
            criteriaList.add(new Criteria("objectCode", Predicate.EQ, targetObjectCode, Relation.AND));
            String targetFieldValueField = targetFieldNameMap().get(targetField);
            criteriaList.add(new Criteria(targetFieldValueField, Predicate.IN, middleFieldData.stream().map(String::valueOf).collect(Collectors.joining(",")), Relation.AND));
            criteriaQuery.setCriteriaList(criteriaList);
            QueryWrapper<DataWarehouse> queryWrapper = getQueryWrapper(targetProjectCode, targetObjectCode, criteriaQuery);
            return dataWarehouseService.list(queryWrapper);
        }
    }
}
